USTC hackergame2018 wp

0x00 写在前面

这次中科大hackergame可以说是既有技术性也有趣味性,总之玩的很开心,而且也学到了很多东西.无论是巧妙应用,还是另类攻击,或者是各种奇怪的算法,以及引入的挖矿和机器学习,都值得学习.下面整理一篇博客记录一下.

0x01 签到类

签到题 50

打开签到题,按照要求输入,发现输入框的长度不够,直接开发者工具,改变maxlength后再次输入即得flag.也可以在地址栏中修改url为?key=hackergame2018

猫咪问答 100

无形之中宣传了一波中科大,然而对于校内和校外的同学来说都需要过硬的社工技巧,基本上百度谷歌全部都可以解决(虽然第三题是爆破的).

游园会的集章卡片 100

丧心病狂的拼图游戏,把图片下载下来之后在桌面上一通操作可得flag.

Word文档 150

下载附件后使用hex friend打开,发现PK头,改后缀为zip,解压之后发现flag.txt,写脚本按行读取拼成flag即可.

0x02 Misc类

猫咪和键盘 150

使用sublime打开cpp文件,然后使用列编辑进行代码还原(网上还有还原的脚本),最后使用g++ -std=c++17 typed_printf.cpp编译并运行即可得到flag.

MacOS系统可以使用Shift+鼠标右键,鼠标中键,command+A 以及 command+shift+L来实现sublime的列编辑.

下面是使用脚本进行还原的过程:

1
2
3
lines=open('typed_printf.cpp','r').readlines()
for line in lines:
print line[0]+line[32:39]+line[1:7]+line[20:22]+line[8:20]+line[22:32]+line[39:-1]

还原后的main函数如下:

oh_my_cat.png-266kB

彩蛋:在开头藏了base64加密过的url,还原后可以在github上找到类似的源码,修改后拿flag.

回到过去 150

由于macOS基于Unix,所以可以直接输入ed命令,但多次尝试并不正确.于是查看ed编辑器的命令,发现Esc按键后的字符不输入,改正后得到flag.

猫咪遥控器 200

下载附件后发现,该txt只包含UDLR,即up,down,left,right这四个英文字母,联想到画图,可以用官解给出的js脚本进行画图,也可以用python的turtle库,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/usr/bin/env python3
import turtle

turtle.screensize()
cat = open('seq.txt','r')
info = cat.read()
x,y = -200,0
for m in info:
if m == 'D':
turtle.goto(x,y-1)
y -= 1
elif m == 'U':
turtle.goto(x,y+1)
y += 1
elif m == 'L':
turtle.goto(x-1,y)
x -= 1
else:
turtle.goto(x+1,y)
x += 1

猫咪电路 200

下载后打开mc游戏,按照提示发现红石电路其实是一堆门组成的电路,用到一些数电上面的逻辑知识,连好电路可得flag.

猫咪克星 200

本题目主要考查eval()函数的危险性.

该题目先使用nc命令对端口进行请求,得到要求为在三十秒内完成python表达式的计算.首先尝试手动输入发现根本输入不完,便考虑采用脚本,详细思路为循环请求并使用eval()函数计算返回的python表达式的结果,再发送至服务器.

当然在解题过程中发现返回的东西并不全部为python数字表达式,而是含有print(),sleep(100),exit(),find~等奇怪的函数,便进行了替换.

脚本一:socket编程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# coding = utf-8
import socket,time
def client_sender():
client = socket.socket()
client.connect(('202.38.95.46',12009))
while True:
result = client.recv(1024).decode()
print(result)
try:
if "flag{" in result:
exit()
send_data = str(eval(result.replace("exit()","0").replace("sleep(100)","sleep(0)").replace("__import__('os').system('find ~')","0").replace("print('\x1b\x5b\x33\x3b\x4a\x1b\x5b\x48\x1b\x5b\x32\x4a')","0")))+'\n'
print(send_data)
client.send(send_data.encode())
except Exception as e:
pass
client.close()
def main():
client_sender()

main()

替换过程使用str.replace()方法.

脚本二:命令行连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import sys
substitute = [
"__import__('os').system('find~')",
"__import__('time').sleep(100)",
r"print('\x1b\x5b\x33\x3b\x4a\x1b\x5b\x48\x1b\x5b\x32\x4a')",
"exit()"
]
def exchange(content):
for i in substitute:
content = content.replace(i,'None')
return content
input()
for j in range(100):
print(j,file = sys.stderr)
exp = input()
print(exp,file = sys.stderr)
answer = eval(exchange(exp))
print(answer,file = sys.stderr)
print(answer)
print(input(),file = sys.stderr)

然后执行nc -e ./filename.py 202.38.95.46 12009或者socat exec:./filename.py tcp:202.38.95.46:12009

替换过程使用字典的key-value对.

滑稽Art

直接使用火狐打开发现每一行对应并不准确,于是参考官方wp,使用wc w filename命令发现该txt文件共154012个文字,使用yafu进行在线分解,经验上,等宽字体字符画像素上的长宽比和字符数的长宽比大致在 1 : 2 左右.所以我们猜测,这个字符画的长宽大概是 556 * 277 或者 574 * 278.使用python编写脚本,每隔556个字符打印’\n’,重新打开新生成的文件发现还原的非常清晰的滑稽图案与flag.

0x03 Web类

猫咪银行 150

仔细观察本题,发现使用正常的方法并不能得到足够的CTB,使用开发者工具审查元素也没有发现什么异常,于是只能在输入框里面做手脚.由于有2s的操作限制,而且每次兑换之后都会同步扣费,所以不考虑条件竞争.构造时间溢出payload如下:

bank1.png-98.6kB
bank2.png-71.7kB

黑曜石浏览器 150

“黑曜石天下第一”表情包的出处大概就是本题了.打开页面发现提示需要用黑曜石浏览器访问.得到思路即为伪装UA头.

下面来获取UA头,网络上搜索只有谷歌会给出黑曜石安装网址.进入该网站发现做的确实不错(除了不能登陆和下载),浏览页面源代码(使用view-source:urlcurl url寻找如何判断是否为黑曜石浏览器的部分,可以得到UA头:

heicore.png-71.1kB

核心判断为判断UA是否为:

Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) HEICORE/49.1.2623.213 Safari/537.36   

拿到了UA之后构造访问,可以使用Chrome的控制台以及命令得到flag:

curl http://202.38.95.46:12001/ -H "User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) HEICORE/49.1.2623.213 Safari/537.36"

哲学思考 150

打开平淡无奇的页面,调用开发者工具的network,刷新页面,查看返回头418 I'm a teapot,便得到答案.

Can I help me? 300

直接打开页面发现,需要使用其他的方法进行请求.当点击链接进入页面时,一般使用的是GET请求.于是我们考虑使用爬虫来实现POST请求.当然,页面会返回:
The method “POST” is deprecated.
See RFC-7168 for more information.
去读RFC-7168,根据:

To this end, a TEA-capable pot that receives a BREW message of content type "message/teapot" MUST respond in accordance with the URI requested, as below.

最终使用BREW请求页面,且带上Content-Type: message/teapot请求头的时候,页面会在Alternative相应头给出真正的URL,再以相同的方式请求那个URL,得到第二个flag.

请求方式可以使用BurpSuite以及Chrome的开发者工具.

can I help me.png-223.1kB

当然也可以使用nc命令:

can I help me2.png-382.1kB

0x04 Crypto类

她的诗 200

先浏览一下解密脚本,发现是uuencode编码,运行一下后发现一篇非常正常的诗句(根据提示这些诗句并没有什么实际效果).检查后发现脚本没有什么问题,想到在线网站,使用uudecode在线网站解密后发现解密结果每一行都会多出来一些符号和字母,将这些东西拼接后得到部分flag.根据英文提示后将fu改成fun},提交即可.(这也是官方wp中的非预期题解).

如同base64使用=进行填充一样,uuencode在加密过程中也需要进行不足位填充,于是便可以在填充位隐藏信息.

修改之后的问题无法通过在线uudecode进行解码,脚本正在研究中.

先贴一个解题脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python3
# This script helps you decode "her poem"

from codecs import decode

fin = open("poem.txt", "r")
fout = open("testpoem.out", "w")

for i in fin:
# print chr(ord(i[0])+4)
# print i[1:]
data = "begin 666 <data>\n" + chr(ord(i[0])+2) + i[1:] + " \nend\n"
decode_data = decode(data.encode("ascii"), "uu")
print(decode_data)[-2:]
fout.write(decode_data.decode("ascii") + "\n")

fin.close()
fout.close()

Z同学的RSA 300

本题主要考察爆破思想.

题目先随机生成了p和q,然后给出了$a=pq^{(p+q)}$和$b=pq^{(p-q)}$这两个大数运算后的结果,再将p*q作为公钥进行加密.

使用数学思想来进行分析发现,从异或的结果推出来e并不在容易处理的范围之内,于是我们想到了逐位爆破:由于a和b是由异或运算生成,所以它们的最低n位与p和q的最低n位有关.我们从最低位开始分别设为0和1进行爆破,这样每增加一位就会多出来四种可能,而a和b的该位又只能确定一种可能,所以计算工作量不会出现指数爆炸的可能,即在运算上是可行的.进行1024次爆破运算可以得到最后的p*q,然后再计算出私钥,进行rsa解密即可.脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import gmpy2

a, b, c = [int(s) for s in open('output.txt').read().split()]
# 定义匿名函数
f1 = lambda p, q: (p * q) ^ (p + q)
f2 = lambda p, q: (p * q) ^ (p - q)

candidates = {(0, 0)}

for m in range(1025):
print(m, len(candidates))
candidates_ = set() # 将candidates_置为空集
mask = (2 << m) - 1 # 经过计算发现,mask分别为1,11,111,1111...
for x, y in candidates:
if f1(x, y) == a and f2(x, y) == b:
# 引用gmpy2库中的invert函数,直接求解出明文
p, q = x, y
d = gmpy2.invert(65537, (p - 1) * (q - 1))
m = pow(c, d, p * q)
print(bytes.fromhex(hex(m)[2:]))
exit()
# 使用两个循环分别置x,y为0和1
for bx in range(2):
for by in range(2):
xx = x + (bx << m) # x为上次爆破出来的低n位,而(bx<<m)则将第n+1位置0或1
yy = y + (by << m)
if f1(xx, yy) & mask != a & mask: # 根据x and 1 = x且 x and 0 = 0,故可以保持低n位不变将n+1到最高位全部置0,这样可以实现比较f1,f2与a,b的低n位
continue
if f2(xx, yy) & mask != b & mask:
continue
candidates_.add((xx, yy))
candidates = candidates_

加密算法和解密算法

研究wp后总算弄懂了大致的加解密过程,特此记录.

打开encrypt.bf文件,发现如下的符号,以两个方括号的内容为界限进行分割.

encryption_and_decryption.png-77kB

根据bf.js对符号进行解析.

1
2
3
4
5
6
','表示在当前指针位置加入新变量  
'>'表示指针向右移动一位
'<'表示指针向左移动一位
'+'表示指针当前位置变量加1
'[]'表示方括号中的内容需要进行数次循环直到第一个指针所指位置的值为0
'.'表示对数值与0的比较结果进行处理

操作之后我们得到一个十一维矩阵[a1, a2, …, a10, 1]

如果我们设输出为b1…b10的话,我们的目标是找到一个十一维的矩阵R(11x11)满足:

[b1, b2, ..., b10, 1] = R(11x11) * [a1, a2, ..., a10, 1]

观察得到的十一维矩阵我们发现该矩阵是定义在整数模64环上的希尔密码的变种,可以使用python的sympy库中的inv_mod()函数直接对某个整数模n环求逆.

贴一下官方代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/usr/bin/python3
import re
import sympy
f = open("encrypt.bf", "r")
bf_code = f.read()
def to_matrix(code):
i = re.compile("[+]+").finditer(code.replace("\n", "")) #使用正则表达式按[]分离
m = [[0 for k in range(11)] for j in range(11)]
for j in range(10):
for k in [0, 2, 4, 6, 8, 9, 7, 5, 3, 1]:
m[j][k] += len(next(i).group(0))
factor = len(next(i).group(0))
for k in [1, 3, 5, 7, 9, 8, 6, 4, 2, 0]:
m[j][k] += factor * len(next(i).group(0))
for k in range(10):
m[10][k] += len(next(i).group(0))
m[10][10] = 1
return m
def decrypt(matrix):
base64_mapping = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"

def to_output(output):
transformed_output = output[:4, :10]
return "".join([base64_mapping[o % 64] for o in transformed_output])

def from_input(input):
transformed_input = [base64_mapping.index(i) for i in input]
return sympy.Matrix(4, 10, transformed_input).row_join(sympy.ones(4, 1))

inv_matrix = sympy.Matrix(matrix).inv_mod(64) #进行模64环上求逆
return lambda input: to_output(from_input(input).multiply(inv_matrix))

if __name__ == "__main__":
fn = decrypt(to_matrix(bf_code))
assert(fn("aMRKoll07lcf49SIuPrNg8v5bMctTkfrQmchaEkF")
== "QUICK_BROWN_FOXES_JUMP_OVER_THE_LAZY_DOG")
assert(fn("p9dJ4Jsrj3oDy_KxMJ1N750NvUBtXVUGNPVALq5l")
== "quick-brown-foxes-jump-over-the-lazy-dog")
assert(fn("ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ")
== "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ")
print("flag{" + fn("JzRVPiVpqo4iDM8celyueIs4ff4DKeG3EMKihzuH") + "}")

0x04 others

本次比赛也出现许多新型的题目,如涉及到区块链、webq类型图片隐写以及机器学习等技术,先将这些题目记录,以备日后学习使用.

请作者吃个小鱼饼干吧